Implementing Inheritance, Polymorphism, and Method Overriding in Swift
Course: iOS App Development
Topic: Implementing Inheritance, Polymorphism, and Method Overriding
Course Link: iOS App Development Training at JustAcademy
Demo Link: Register for a Free Course Demo
Official Website: JustAcademy
1. Introduction
Inheritance, polymorphism, and method overriding are important object-oriented programming concepts used in Swift and native iOS app development. They help developers organize related classes, reuse existing code, and customize behavior for different types of objects.
For example, an iOS application might contain different types of users, such as regular users, premium users, and administrators. These users may share common properties, including a name and email address, while having different permissions or behaviors. Object-oriented programming allows common functionality to be shared while specialized behavior is implemented where needed.
Swift supports class inheritance and method overriding. Polymorphic behavior can be achieved through class hierarchies and is also commonly modeled using protocols. Read the Swift inheritance guide.
2. Learning Objectives
- Understand inheritance and its role in object-oriented programming.
- Identify superclass and subclass relationships in Swift.
- Create subclasses using Swift class syntax.
- Understand method overriding and the
override keyword.
- Use the
super keyword to access superclass implementations.
- Understand polymorphism through superclass references.
- Distinguish inheritance, overriding, and overloading.
- Apply these concepts to practical iOS app models.
- Recognize when composition and protocols are better alternatives to inheritance.
3. Understanding Inheritance
Inheritance is an object-oriented programming feature that allows one class to inherit properties and methods from another class. It helps avoid duplicating common functionality across related classes.
In Swift, inheritance is supported by classes. Structures and enumerations do not inherit from classes, although they can adopt protocols.
3.1 Important Terminology
| Term |
Meaning |
Example |
| Superclass |
The parent class whose accessible members may be inherited. |
Vehicle |
| Subclass |
The child class that inherits from a superclass. |
Car |
| Inheritance |
The relationship in which a subclass receives inherited characteristics from a superclass. |
class Car: Vehicle |
| Method |
A function defined inside a type. |
func start() |
| Overriding |
Providing a new implementation of an inherited member. |
override func start() |
| Polymorphism |
Using a common interface or superclass reference to work with different concrete types. |
Different vehicles implementing their own start behavior |
3.2 Benefits of Inheritance
- Code reuse: Shared properties and methods can be defined once in a superclass.
- Maintainability: Common behavior can be updated in one place.
- Specialization: Subclasses can add properties and methods for their specific requirements.
- Extensibility: New subclasses can be added without rewriting all common functionality.
- Polymorphic design: Different subclasses can be processed through a common superclass type.
4. Creating a Base Class in Swift
A class that does not inherit from another class is known as a base class. It can define common properties and methods that related subclasses can reuse.
class Vehicle {
var brand: String
var speed: Double
init(brand: String, speed: Double = 0.0) {
self.brand = brand
self.speed = speed
}
func start() {
print("\(brand) vehicle is starting.")
}
func displayDetails() {
print("Brand: \(brand)")
print("Speed: \(speed) km/h")
}
}
let vehicle = Vehicle(brand: "Generic")
vehicle.start()
vehicle.displayDetails()
Explanation
Vehicle is the class name.
brand stores the vehicle brand.
speed stores the current speed.
init() initializes the stored properties.
start() defines common starting behavior.
displayDetails() prints the vehicle's information.
The Vehicle class provides reusable functionality. More specific vehicle classes can inherit this behavior and customize it.
5. Creating a Subclass
In Swift, a subclass names its superclass after a colon in the class declaration. The subclass inherits accessible properties and methods and can introduce its own members.
class Car: Vehicle {
var numberOfDoors: Int
init(brand: String, numberOfDoors: Int) {
self.numberOfDoors = numberOfDoors
super.init(brand: brand)
}
func openTrunk() {
print("\(brand) car trunk is open.")
}
}
let car = Car(brand: "Toyota", numberOfDoors: 4)
car.start()
car.displayDetails()
car.openTrunk()
print("Doors: \(car.numberOfDoors)")
Explanation
class Car: Vehicle declares Car as a subclass of Vehicle.
numberOfDoors is a property specific to the car.
super.init(brand: brand) calls the superclass initializer to initialize the inherited brand property.
- The car can use the inherited
start() and displayDetails() methods.
openTrunk() is an additional method defined only for cars.
Notice that the subclass does not need to copy the implementation of displayDetails() or start() to use them.
6. Understanding the super Keyword
The super keyword provides access to an appropriate superclass implementation. It is particularly useful when a subclass initializer needs to initialize its superclass or when an overridden method should preserve some parent-class behavior.
6.1 Calling a Superclass Method
class Vehicle {
func start() {
print("Vehicle system activated.")
}
}
class ElectricCar: Vehicle {
override func start() {
super.start()
print("Electric motor is ready.")
}
}
let electricCar = ElectricCar()
electricCar.start()
Output:
Vehicle system activated.
Electric motor is ready.
The overridden method first calls super.start(), then adds electric-car-specific behavior. If the subclass does not need the superclass implementation, it can provide a completely different implementation instead.
6.2 When to Use super
- When a subclass initializer must call a superclass initializer.
- When an overridden method should retain the original behavior and add more functionality.
- When an overridden property getter or setter needs to use the superclass implementation.
- When extending an existing class without duplicating its original logic.
7. Understanding Method Overriding
Method overriding occurs when a subclass provides its own implementation of an inherited method. The method being overridden must be eligible for overriding, and the subclass uses the override keyword to make its intention explicit.
The Swift compiler checks that an overridden declaration matches an appropriate superclass declaration. Using override when no valid inherited member exists is a compile-time error.
7.1 Basic Method Overriding Example
class Animal {
func makeSound() {
print("Animal makes a sound.")
}
}
class Dog: Animal {
override func makeSound() {
print("Dog barks.")
}
}
class Cat: Animal {
override func makeSound() {
print("Cat meows.")
}
}
let animal = Animal()
let dog = Dog()
let cat = Cat()
animal.makeSound()
dog.makeSound()
cat.makeSound()
Output:
Animal makes a sound.
Dog barks.
Cat meows.
7.2 How Overriding Works
- The superclass defines an inherited method named
makeSound().
- The
Dog subclass overrides the method with dog-specific behavior.
- The
Cat subclass overrides the same method with cat-specific behavior.
- Calling the method on each instance executes the implementation appropriate to that instance's runtime type.
7.3 Rules for Method Overriding
- Use the
override keyword for a valid overridden superclass method.
- The overridden method must match the inherited declaration's requirements, including its name, parameters, and return type.
- The superclass member must be accessible and eligible for overriding.
- A
final method cannot be overridden.
- Use
super.methodName() when the parent implementation should also run.
- Method overriding is different from defining an unrelated method with a different signature.
8. Overriding Properties in Swift
Swift also permits eligible inherited properties to be overridden. A subclass may provide a custom getter and, where allowed, a setter for an inherited property. It can also add property observers to monitor changes.
class Employee {
var roleDescription: String {
return "General employee"
}
}
class Developer: Employee {
override var roleDescription: String {
return "Software developer"
}
}
let employee = Employee()
let developer = Developer()
print(employee.roleDescription)
print(developer.roleDescription)
Output:
General employee
Software developer
The subclass provides its own computed-property implementation while keeping the same property name and type. Property overrides must follow Swift's access and type requirements.
9. Understanding Polymorphism
Polymorphism means that code can work with different concrete types through a shared abstraction or common type. In inheritance-based polymorphism, a variable or collection typed as a superclass can refer to instances of its subclasses.
When an overridable instance method is called through a superclass reference, Swift can execute the implementation belonging to the actual object type. This allows a single piece of code to handle multiple specialized objects.
9.1 Polymorphism Example
class Shape {
func calculateArea() -> Double {
return 0.0
}
}
class Rectangle: Shape {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
override func calculateArea() -> Double {
return width * height
}
}
class Circle: Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
override func calculateArea() -> Double {
return Double.pi * radius * radius
}
}
let shapes: [Shape] = [
Rectangle(width: 5, height: 4),
Circle(radius: 3)
]
for shape in shapes {
print("Area: \(shape.calculateArea())")
}
Output:
Area: 20.0
Area: 28.274333882308138
9.2 Why This Is Polymorphism
- The array has the common type
[Shape].
- It stores both
Rectangle and Circle objects.
- The loop calls the same method,
calculateArea(), on each element.
- The result depends on the concrete object and its overridden implementation.
This design avoids writing a separate loop for every shape type. Adding another subclass, such as Triangle, can extend the behavior while preserving the same calling pattern.
10. Inheritance, Overriding, and Polymorphism Compared
| Concept |
Main Purpose |
Swift Mechanism |
Example |
| Inheritance |
Reuse and specialize class functionality. |
Subclass declaration |
class Car: Vehicle |
| Method overriding |
Replace or extend inherited behavior. |
override keyword |
override func start() |
| Polymorphism |
Use different concrete types through a common type. |
Superclass references or protocol existentials |
let shapes: [Shape] |
These concepts are related but not interchangeable. Inheritance establishes a class relationship, overriding customizes inherited behavior, and polymorphism enables code to use a common type while supporting different implementations.
11. Method Overriding vs. Method Overloading
Method overriding and method overloading are different concepts. Overriding replaces an inherited implementation, while overloading defines multiple functions with the same name but different parameter signatures.
| Feature |
Overriding |
Overloading |
| Relationship |
Requires an eligible inherited member. |
Does not require inheritance. |
| Purpose |
Customize inherited behavior. |
Support different parameter combinations. |
| Keyword |
Uses override. |
Does not use override merely for overloading. |
| Example |
A subclass overrides display(). |
Functions named display() accept different parameters. |
Overloading Example
class MessageFormatter {
func format(_ value: String) -> String {
return "Message: \(value)"
}
func format(_ value: Int) -> String {
return "Number: \(value)"
}
}
let formatter = MessageFormatter()
print(formatter.format("Hello"))
print(formatter.format(100))
Output:
Message: Hello
Number: 100
12. Preventing Inheritance and Overriding with final
Swift provides the final keyword to prevent further specialization in situations where subclassing or overriding is not intended.
class Account {
final func validateAccount() {
print("Account validation completed.")
}
}
class SavingsAccount: Account {
// Cannot override validateAccount()
// because it is marked final.
}
final class SecurityManager {
func authenticate() {
print("User authenticated.")
}
}
// A class cannot inherit from SecurityManager.
A final method cannot be overridden. A final class cannot be subclassed. Use these restrictions when they communicate an intentional design boundary; do not apply them without considering future extension requirements.
13. Applying These Concepts to iOS App Development
Inheritance and polymorphism can help organize models and behaviors in iOS applications. For example, an app may contain several types of notifications that share a common interface but display different messages.
13.1 Notification Example
class AppNotification {
var title: String
init(title: String) {
self.title = title
}
func messageText() -> String {
return title
}
}
class ReminderNotification: AppNotification {
var reminderTime: String
init(title: String, reminderTime: String) {
self.reminderTime = reminderTime
super.init(title: title)
}
override func messageText() -> String {
return "\(title) at \(reminderTime)"
}
}
class PromotionNotification: AppNotification {
var discountPercent: Int
init(title: String, discountPercent: Int) {
self.discountPercent = discountPercent
super.init(title: title)
}
override func messageText() -> String {
return "\(title) - Save \(discountPercent)%!"
}
}
let notifications: [AppNotification] = [
ReminderNotification(
title: "Team meeting",
reminderTime: "10:30 AM"
),
PromotionNotification(
title: "Special offer",
discountPercent: 20
)
]
for notification in notifications {
print(notification.messageText())
}
Output:
Team meeting at 10:30 AM
Special offer - Save 20%!
13.2 Using the Result in a SwiftUI View
The following example demonstrates how a SwiftUI view can display the messages produced by different notification subclasses. The sample assumes the preceding notification classes are defined in the same app target.
import SwiftUI
struct NotificationListView: View {
private let notifications: [AppNotification] = [
ReminderNotification(
title: "Project review",
reminderTime: "2:00 PM"
),
PromotionNotification(
title: "Premium plan",
discountPercent: 15
)
]
var body: some View {
NavigationStack {
List(notifications, id: \.title) { notification in
VStack(alignment: .leading, spacing: 6) {
Text(notification.title)
.font(.headline)
Text(notification.messageText())
.font(.subheadline)
}
.padding(.vertical, 4)
}
.navigationTitle("Notifications")
}
}
}
This illustrates how a common superclass can let a view handle multiple specialized objects. For production applications, use stable unique identifiers for list rows if titles may repeat, and consider value types or protocols when inheritance is unnecessary.
14. Choosing Between Class Inheritance and Protocols
Inheritance is useful when types have a genuine hierarchical relationship and should share state or implementation. However, inheritance can tightly couple subclasses to the design of their superclass.
Swift protocols offer an alternative: they describe capabilities that classes, structures, and enumerations can adopt. A type can conform to multiple protocols, while a class can have only one direct superclass.
| Choose Inheritance When |
Choose Protocols When |
| Types have a meaningful parent-child relationship. |
Different kinds of types need to share a capability. |
| Shared state and implementation are appropriate. |
You want to describe required behavior without forcing a class hierarchy. |
| Subclasses need to customize inherited methods. |
Structures and enumerations should also support the abstraction. |
| A superclass reference is useful for polymorphic behavior. |
A type needs to conform to several independent interfaces. |
Protocol-Based Example
protocol Notifiable {
func messageText() -> String
}
struct EmailNotification: Notifiable {
let subject: String
func messageText() -> String {
return "Email: \(subject)"
}
}
struct PushNotification: Notifiable {
let message: String
func messageText() -> String {
return "Push: \(message)"
}
}
let messages: [any Notifiable] = [
EmailNotification(subject: "Project update"),
PushNotification(message: "New activity")
]
for item in messages {
print(item.messageText())
}
Both value types conform to Notifiable without inheriting from a common class. The any Notifiable type allows the array to hold values of different concrete types that conform to this protocol.
15. Common Errors and Troubleshooting
| Error or Problem |
Cause |
Solution |
Missing override |
A subclass method matches an inherited method but is not marked as an override. |
Add override to the declaration. |
| Invalid override |
The superclass does not contain an eligible matching member. |
Check the method name, parameters, return type, access level, and superclass declaration. |
| Cannot override a final method |
The superclass member is marked final. |
Use a different extension point or redesign the superclass if overriding is appropriate. |
| Initializer error |
A subclass has not initialized its own stored properties or called the required superclass initializer in the correct order. |
Initialize subclass properties and follow Swift's initializer rules. |
| Superclass behavior does not execute |
The overriding method does not call the parent implementation. |
Call super.methodName() if the original behavior is needed. |
| Incorrect output for a subclass |
The subclass may not override the intended method or the call may target a different method. |
Verify the declaration, instance type, and method being called. |
| Too many inheritance levels |
Shared behavior is being modeled with a deep hierarchy. |
Consider protocols, composition, and smaller reusable components. |
16. Best Practices
- Keep superclass responsibilities focused and understandable.
- Use inheritance only when the relationship between types is meaningful.
- Use the
override keyword explicitly and let the compiler validate overrides.
- Call
super when the parent implementation is part of the intended behavior.
- Use
final to prevent extension only when that restriction is deliberate.
- Prefer protocols for shared capabilities across unrelated types.
- Keep overridden methods consistent with the expectations established by the superclass.
- Avoid making subclasses depend on implementation details they should not need.
- Test each subclass independently and test polymorphic behavior through the common type.
- Use descriptive class and method names, such as
PaymentMethod, CardPayment, and processPayment().
17. Practical Exercises
Exercise 1: Vehicle Hierarchy
- Create a base class named
Vehicle with properties for brand and speed.
- Create
Car and Motorcycle subclasses.
- Give each subclass a specific method.
- Override a common method to produce different output for each type.
Exercise 2: Employee Management
- Create an
Employee superclass with a name and a method called calculateBonus().
- Create
FullTimeEmployee and ContractEmployee subclasses.
- Override the bonus calculation to reflect different rules.
- Store both objects in an array typed as
[Employee] and process them in a loop.
Exercise 3: iOS Notification System
- Create a common notification abstraction.
- Implement reminder and promotional notifications.
- Override a method to generate each notification's display text.
- Display the generated text in a SwiftUI list.
- Consider whether a protocol would be a better fit than a superclass.
18. Mini Project: Payment Method Simulator
Project objective: Build a small Swift program that demonstrates inheritance, method overriding, and polymorphism through different payment methods.
class PaymentMethod {
var accountName: String
init(accountName: String) {
self.accountName = accountName
}
func pay(amount: Double) {
print("\(accountName) is processing payment.")
}
}
class CardPayment: PaymentMethod {
override func pay(amount: Double) {
print("Card payment of \(amount) processed.")
}
}
class UpiPayment: PaymentMethod {
override func pay(amount: Double) {
print("UPI payment of \(amount) processed.")
}
}
let paymentMethods: [PaymentMethod] = [
CardPayment(accountName: "Personal Card"),
UpiPayment(accountName: "Personal UPI")
]
for method in paymentMethods {
method.pay(amount: 500)
}
Expected output:
Card payment of 500.0 processed.
UPI payment of 500.0 processed.
Extension tasks: Add a wallet payment subclass, validate payment amounts, and write unit tests for each payment implementation. In a real payment application, payment authorization, secure data handling, and transaction processing would require dedicated production-ready services rather than these demonstration classes.
19. Interview Questions and Answers
Q1. What is inheritance in Swift?
Inheritance allows a class to inherit accessible properties and methods from a superclass and specialize them in a subclass.
Q2. Does Swift support multiple class inheritance?
No. A class can inherit directly from only one superclass, but it can conform to multiple protocols.
Q3. What is method overriding?
Method overriding occurs when a subclass supplies a new implementation of an eligible inherited method.
Q4. Why is the override keyword required?
It explicitly communicates that a declaration overrides an inherited member and allows the compiler to verify that the override is valid.
Q5. What is the purpose of super?
The super keyword accesses an appropriate superclass implementation, such as calling a parent method or initializer.
Q6. What is polymorphism?
Polymorphism allows code to operate through a common type while different concrete types provide their own behavior.
Q7. Can a subclass override a final method?
No. A method marked final cannot be overridden.
Q8. Can a structure inherit from a class?
No. Swift structures do not inherit from classes, but they can adopt protocols.
Q9. What is the difference between overriding and overloading?
Overriding changes inherited behavior in a subclass. Overloading defines multiple functions with the same name but different parameter signatures.
Q10. When should protocols be preferred over inheritance?
Protocols are often preferable when unrelated types need to share a capability or when value types should participate in the same abstraction.
Q11. Why might a subclass call super inside an overridden method?
It allows the subclass to preserve the superclass behavior and then add or customize its own functionality.
Q12. How does polymorphism help in iOS development?
It lets app code process different models or behaviors through a common superclass or protocol, reducing type-specific branching and making the design easier to extend.
20. Quick Revision Checklist
- Inheritance allows one class to derive functionality from another class.
- The parent class is called the superclass; the child class is called the subclass.
- Use
class Subclass: Superclass to establish a class inheritance relationship.
- Use
override to provide a new implementation of an eligible inherited member.
- Use
super when the superclass implementation is required.
- Polymorphism lets different concrete types be handled through a common type.
- Use
final to prevent a class or member from being extended through inheritance or overriding.
- Overriding and overloading are distinct concepts.
- Swift supports one superclass per class and multiple protocol conformances.
- Prefer clear, shallow inheritance hierarchies and consider protocols or composition where appropriate.
21. Conclusion
Inheritance, polymorphism, and method overriding are foundational Swift concepts for designing reusable and extensible iOS applications. Inheritance provides a way to share functionality between related classes, overriding allows subclasses to customize inherited behavior, and polymorphism lets the same code work with different concrete types.
Practicing these concepts with models such as vehicles, notifications, employees, shapes, and payment methods will help you build a stronger understanding of object-oriented programming. In production iOS projects, choose inheritance deliberately and use protocols and composition whenever they provide a simpler and more flexible design.
22. Additional Learning Resources